React Hooks

以前函数组件也被称为无状态的组件,只负责渲染的一些工作。hooks的出现,使函数组件的功能得到了扩充,拥有了类组件相似的功能,在我们日常使用中,使用hooks能够解决大多数问题,并且还拥有代码复用机制,因此优先考虑hooks

解决的问题

!!!状态逻辑复用!!!

  1. 在组件之间复用状态逻辑很难。React 没有提供将可复用性行为“附加”到组件的途径(例如,把组件连接到 store)。有一些解决此类问题的方案,比如 render props 和 高阶组件。但是这类方案需要重新组织你的组件结构,这可能会很麻烦,使你的代码难以理解。
  2. 复杂组件变得难以理解。组件常常在 componentDidMount 和 componentDidUpdate 中获取数据。但是,同一个 componentDidMount 中可能也包含很多其它的逻辑,如设置事件监听,而之后需在 componentWillUnmount 中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。
  3. Class 语法的使用不友好。class 是学习 React 的一大屏障。你必须去理解 JavaScript 中 this 的工作方式,还不能忘记绑定事件处理器。没有稳定的语法提案,这些代码非常冗余。大家可以很好地理解 props,state 和自顶向下的数据流,但对 class 却一筹莫展。
  • hooks能够更容易解决状态相关的重用的问题,每调用 useHook 一次都会生成一份独立的状态。
  • 通过自定义 hook 能够更好的封装我们的功能,编写hooks为函数式编程,每个功能都包裹在函数中,整体风格更清爽,更优雅。

useEffect

组件在加载和更新阶段都执行同样操作 (componentDidMount/componentDidUpdate)。如果使用 useEffect 后,则能够将相同的逻辑抽离出来,这是类组件不具备的方法。

如果某些特定值在两次重渲染之间没有发生变化,可以跳过对 effect 的调用,只需要传入第二个参数:

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

回调函数中可以返回一个清除函数,这是 effect 可选的清除机制,相当于类组件中 componentWillUnmount 生命周期函数,可做一些清除副作用的操作,如下:

useEffect(() => {
  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
});

所以, useEffect 相当于 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个生命周期函数的组合

useEffect 的回调函数为什么无法使用 async?

React 本身并不支持这么做,理由是 effect function 应该返回一个销毁函数(effect:是指 return 返回的 cleanup 函数),如果 useEffect 第一个参数传入 async,返回值则变成了 Promise,会导致 react 在调用销毁函数的时候报错:function.apply is undefined。

解决方式:把异步提取成单独函数或自定义 hook(推荐做法)

与 useLayoutEffect 的区别???

useMemo

useMemo 和 memo 的区别是什么?

React.memo() 本质是一个高阶组件(HOC),高阶组件和高阶函数类似,高阶函数是接收一个函数,然后经过一些判断和处理后再返回这个函数,比如我们常见的防抖、节流函数。对应到高阶组件,就是接收一个组件,然后经过一些判断和处理后再返回这个组件。

React.memo(), 这个高阶组件接收一个组件 A 作为参数并返回一个组件 B,如果组件 B 的 props 没有改变,则组件 B 会阻止组件 A 重新渲染。A 和 B 本质上是同一个组件,但 A 是否进行重新渲染,需要由 Props 是否发生改变来决定。

react hooks 的原理和实现

useState 的实现原理

当调用 useState 的时候,会返回形如 (变量, 函数) 的一个元祖。并且 state 的初始值就是外部调用 useState 的时候,传入的参数。

理清楚了传参和返回值,再来看下 useState 还做了些什么。正如下面代码所示,当点击按钮的时候,执行setNum,状态 num 被更新,并且 UI 视图更新。显然,useState 返回的用于更改状态的函数,自动调用了render方法来触发视图更新。

function App() {
  const [num, setNum] = useState < number > 0;

  return (
    <div>
      <div>num: {num}</div>
      <button onClick={() => setNum(num + 1)}>1</button>
    </div>
  );
}

有了上面的探索,借助闭包,封装一个 setState 如下:

function render() {
  ReactDOM.render(<App />, document.getElementById("root"));
}

let state: any;

function useState<T>(initialState: T): [T, (newState: T) => void] {
  state = state || initialState;

  function setState(newState: T) {
    state = newState;
    render();
  }

  return [state, setState];
}

render(); // 首次渲染

这是一个简易能用的useState雏形了。它也解决了文章开始提到的「🤔️ useState 的实现原理」这个问题。但如果在函数内声明多个 state,在当前代码中,只有第一个 state 是生效的(请看state = state || initialState;))。

为什么不能在循环、判断内部使用 Hook

先不要考虑题目提及的问题。思路还是回到如何让 useState 支持多个 state。《React hooks: not magic, just arrays》open in new window中提及,React Hook 看起来非常 Magic 的实现,本质上还是通过 Array 来实现的。

前面 useState 的简单实现里,初始的状态是保存在一个全局变量中的。以此类推,多个状态,应该是保存在一个专门的全局容器中。这个容器,就是一个朴实无华的 Array 对象。具体过程如下:

  • 第一次渲染时候,根据 useState 顺序,逐个声明 state 并且将其放入全局 Array 中。每次声明 state,都要将 cursor 增加 1。
  • 更新 state,触发再次渲染的时候。cursor 被重置为 0。按照 useState 的声明顺序,依次拿出最新的 state 的值,视图更新。

请看下面这张图,每次使用 useState,都会向 STATE 容器中添加新的状态。

实现的代码如下:

import React from "react";
import ReactDOM from "react-dom";

const states: any[] = [];
let cursor: number = 0;

function useState<T>(initialState: T): [T, (newState: T) => void] {
  const currenCursor = cursor;
  states[currenCursor] = states[currenCursor] || initialState; // 检查是否渲染过

  function setState(newState: T) {
    states[currenCursor] = newState;
    render();
  }

  ++cursor; // update: cursor
  return [states[currenCursor], setState];
}

function App() {
  const [num, setNum] = useState < number > 0;
  const [num2, setNum2] = useState < number > 1;

  return (
    <div>
      <div>num: {num}</div>
      <div>
        <button onClick={() => setNum(num + 1)}>1</button>
        <button onClick={() => setNum(num - 1)}>1</button>
      </div>
      <hr />
      <div>num2: {num2}</div>
      <div>
        <button onClick={() => setNum2(num2 * 2)}>扩大一倍</button>
        <button onClick={() => setNum2(num2 / 2)}>缩小一倍</button>
      </div>
    </div>
  );
}

function render() {
  ReactDOM.render(<App />, document.getElementById("root"));
  cursor = 0; // 重置cursor
}

render(); // 首次渲染

此时,如果想在循环、判断等不在函数组件顶部的地方使用 Hook,如下所示:

let tag = true;

function App() {
  const [num, setNum] = useState < number > 0;

  // 只有初次渲染,才执行
  if (tag) {
    const [unusedNum] = useState < number > 1;
    tag = false;
  }

  const [num2, setNum2] = useState < number > 2;

  return (
    <div>
      <div>num: {num}</div>
      <div>
        <button onClick={() => setNum(num + 1)}>1</button>
        <button onClick={() => setNum(num - 1)}>1</button>
      </div>
      <hr />
      <div>num2: {num2}</div>
      <div>
        <button onClick={() => setNum2(num2 * 2)}>扩大一倍</button>
        <button onClick={() => setNum2(num2 / 2)}>缩小一倍</button>
      </div>
    </div>
  );
}

由于在条件判断的逻辑中,重置了tag=false,因此此后的渲染不会再进入条件判断语句。看起来好像没有问题?但是,由于 useState 是基于 Array+Cursor 来实现的,第一次渲染时候,state 和 cursor 的对应关系如下表:

变量名cursor
num0
unusedNum1
num22

当点击事件触发再次渲染,并不会进入条件判断中的 useState。所以,cursor=2 的时候对应的变量是 num2。而其实 num2 对应的 cursor 应该是 3。就会导致setNum2并不起作用。

到此,解决了文章开头提出的「🤔️ 为什么不能在循环、判断内部使用 Hook」。在使用 Hook 的时候,请在函数组件顶部使用!

useEffect 的实现原理

在探索 useEffect 原理的时候,一直被一个问题困扰:useEffect 作用和用途是什么?当然,用于函数的副作用这句话谁都会讲。举个例子吧:

function App() {
  const [num, setNum] = useState(0);

  useEffect(() => {
    // 模拟异步请求后端数据
    setTimeout(() => {
      setNum(num + 1);
    }, 1000);
  }, []);

  return <div>{!num ? "请求后端数据..." : `后端数据是 ${num}`}</div>;
}

这段代码,虽然这样组织可读性更高,毕竟可以将这个请求理解为函数的副作用。但这并不是必要的。完全可以不使用useEffect,直接使用setTimeout,并且它的回调函数中更新函数组件的 state。

在阅读A Complete Guide to useEffectopen in new window构建你自己的 Hooksopen in new window之后,我才理解 useEffect 的存在的必要性和意义。

在 useEffect 的第二个参数中,我们可以指定一个数组,如果下次渲染时,数组中的元素没变,那么就不会触发这个副作用(可以类比 Class 类的关于 nextprops 和 prevProps 的生命周期)。好处显然易见,相比于直接裸写在函数组件顶层,useEffect 能根据需要,避免多余的 render

下面是一个不包括销毁副作用功能的 useEffect 的 TypeScript 实现:

// 还是利用 Array + Cursor的思路
const allDeps: any[][] = [];
let effectCursor: number = 0;

function useEffect(callback: () => void, deps: any[]) {
  if (!allDeps[effectCursor]) {
    // 初次渲染:赋值 + 调用回调函数
    allDeps[effectCursor] = deps;
    ++effectCursor;
    callback();
    return;
  }

  const currenEffectCursor = effectCursor;
  const rawDeps = allDeps[currenEffectCursor];
  // 检测依赖项是否发生变化,发生变化需要重新render
  const isChanged = rawDeps.some(
    (dep: any, index: number) => dep !== deps[index]
  );
  if (isChanged) {
    callback();
  }
  ++effectCursor;
}

function render() {
  ReactDOM.render(<App />, document.getElementById("root"));
  effectCursor = 0; // 注意将 effectCursor 重置为0
}

对于 useEffect 的实现,配合下面案例的使用会更容易理解。当然,你也可以在这个 useEffect 中发起异步请求,并在接受数据后,调用 state 的更新函数,不会发生爆栈的情况。

function App() {
  const [num, setNum] = useState < number > 0;
  const [num2] = useState < number > 1;

  // 多次触发
  // 每次点击按钮,都会触发 setNum 函数
  // 副作用检测到 num 变化,会自动调用回调函数
  useEffect(() => {
    console.log("num update: ", num);
  }, [num]);

  // 仅第一次触发
  // 只会在compoentDidMount时,触发一次
  // 副作用函数不会多次执行
  useEffect(() => {
    console.log("num2 update: ", num2);
  }, [num2]);

  return (
    <div>
      <div>num: {num}</div>
      <div>
        <button onClick={() => setNum(num + 1)}>1</button>
        <button onClick={() => setNum(num - 1)}>1</button>
      </div>
    </div>
  );
}

⚠️ useEffect 第一个回调函数可以返回一个用于销毁副作用的函数,相当于 Class 组件的 unmount 生命周期。这里为了方便说明,没有进行实现。

在这一小节中,尝试解答了 「🤔️ useEffect 的实现原理」和 「🤔️ useEffect 的应用场景」这两个问题。

Class VS Hooks

虽然 Hooks 看起来更酷炫,更简洁。但是在实际开发中我更倾向于使用 Class 来声明组件。两种方法的对比如下:

ClassHooks
代码逻辑清晰(构造函数、componentDidMount 等)需要配合变量名和注释
不容易内存泄漏容易发生内存泄漏

总的来说,Hooks 对代码编写的要求较高,在没有有效机制保证代码可读性、规避风险的情况下,Class 依然是我的首选。关于内存泄漏,下面是一个例子(目前还没找到方法规避这种向全局传递状态更新函数的做法):

import React, { useState } from "react";
import ReactDOM from "react-dom";

let func: any;
setInterval(() => {
  typeof func === "function" && func(Date.now());
  console.log("interval");
}, 1000);

function App() {
  const [num, setNum] = useState < number > 0;
  if (typeof func !== "function") {
    func = setNum;
  }
  return <div>{num}</div>;
}

function render() {
  ReactDOM.render(<App />, document.getElementById("root"));
}

render();

react 里面 useReducer 和 useState 区别

useState 和 useReducer 都是用于管理组件内部的状态的 Hook。 1、API 使用: useState:useState 是 React 提供的最基本的状态管理 Hook,通过调用该 Hook,可以在函数组件中定义和更新状态。它返回一个包含状态值和更新状态的函数的数组。更新状态的函数通常命名为 setXxx(例如,setCount)。 useReducer:useReducer 是另一个状态管理 Hook,它提供了一种将状态和状态更新逻辑封装在一起的方式。它接受一个 reducer 函数和初始状态,并返回一个包含当前状态和派发(dispatch)函数的数组。 2、状态管理: useState:useState 适用于简单的状态管理,每次更新状态都是独立的,互不影响。调用 setXxx 函数会触发组件的重新渲染,并将新的状态值应用到对应的状态变量上。 useReducer:useReducer 适用于更复杂的状态管理和逻辑处理。它将状态和状态更新逻辑放在 reducer 函数中,reducer 函数接收当前状态和一个 action 对象,并返回新的状态。通过调用派发函数(dispatch),可以触发相应的状态更新逻辑。 3、状态更新的触发: useState:useState 的状态更新是通过调用 setXxx 函数来触发的。每次调用 setXxx 都会重新渲染组件,这是因为 useState 返回的更新状态的函数在内部使用了浅比较,以判断状态是否发生了改变。 useReducer:useReducer 的状态更新是通过调用派发函数来触发的,该函数接收一个 action 对象作为参数。调用派发函数不会自动重新渲染组件,需要在 reducer 函数中返回新的状态才能触发重新渲染。

useState 适用于简单的状态管理,适合于单一的状态值和更新逻辑。而 useReducer 则适用于更复杂的状态管理和逻辑处理,通过 reducer 函数将状态和状态更新逻辑封装在一起。

react 后面本地启动严格模式为什么 useEffect 执行两次

严格模式(Strict Mode)是一种用于检测并警告潜在问题的开发工具。它会对组件进行两次渲染,以捕获可能产生副作用的代码。useEffect 是一个副作用 Hook,用于处理与组件渲染无关的操作,例如数据获取、订阅或 DOM 操作。

当启用严格模式后,React 在第一次渲染时会执行正常的渲染流程,然后在第二次渲染时会检测在首次渲染期间是否有任何副作用产生了变化。如果有变化,React 会发出警告,以帮助你发现可能导致 bug 的问题。

这就是为什么在严格模式下,useEffect 可能会执行两次的原因。第一次渲染是为了收集可能的副作用,并调用 useEffect 中定义的回调函数,第二次渲染是为了检查是否有意外的变化。

需要注意的是,在生产环境中使用 React,严格模式是被禁用的,因此在生产环境中使用 useEffect 不会出现两次执行的情况。

Hooks 面试问题

useCallback 和 useDemo 的区别

useMemo  和  useCallback  接收的参数都是一样,第一个参数为回调 第二个参数为要依赖的数据

共同作用:仅仅   依赖数据   发生变化, 才会重新计算结果,也就是起到缓存的作用。

useCallback 和 useMemo 都可缓存函数的引用或值,但是从更细的使用角度来说 useCallback 缓存函数的引用,useMemo 缓存计算数据的值。

两者区别:

1.useMemo  计算结果是  return  回来的值, 主要用于 缓存计算结果的值 ,应用场景如: 需要计算的状态

2.useCallback  计算结果是   函数, 主要用于 缓存函数,应用场景如: 需要缓存的函数,因为函数式组件每次任何一个 state 的变化 整个组件 都会被重新刷新,一些函数是没有必要被重新刷新的,此时就应该缓存起来,提高性能,和减少资源浪费。

注意: 不要滥用会造成性能浪费,react 中减少 render 就能提高性能,所以这个仅仅只针对缓存能减少重复渲染时使用和缓存计算结果。

hooks 的实现原理

修改核心是将 useState,useEffect 按照调用的顺序放入 memoizedState 中,每次更新时,按照顺序进行取值和判断逻辑,我们根据调用 hook 顺序,将 hook 依次存入数组 memoizedState 中,每次存入时都是将当前的 currentCursor 作为数组的下标,将其传入的值作为数组的值,然后在累加 currentCursor hook 的状态值都被存入数组中 memoizedState。先将旧数组 memoizedState 中对应的值取出来重新复值,从而生成新数组 memoizedState。对于是否执行 useEffect 通过判断其第二个参数是否发生变化而决定的。

这里我们就知道了为啥不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们。因为我们是根据调用 hook 的顺序依次将值存入数组中,如果在判断逻辑循环嵌套中,就有可能导致更新时不能获取到对应的值,从而导致取值混乱。同时 useEffect 第二个参数是数组,也是因为它就是以数组的形式存入的。

Hooks 闭包

useEffect、useMemo、useCallback 都是自带闭包的。每一次组件的渲染,它们都会捕获当前组件函数上下文中的状态(state, props),所以每一次这三种 hooks 的执行,反映的也都是当前的状态,你无法使用它们来捕获上一次的状态。

Hooks 体验

  1. hooks 对 TypeScript 支持更加友好。例如 useState 对应的状态类型可以被自动推断出来、useEffect 不需要写类型声明(class 写法下几个生命周期函数的参数签名写起来是相当繁琐的),hooks 能帮我们省去大量手动声明类型的操作。同时 hooks 让重构更加简单,使用 hooks 的代码中不会出现 this,组件的 props 或是 state 在组件代码中都是普通的变量,减少了重构的成本,例如原先 重命名 this.state 中某个字段 ,在 hooks 下就成了 重命名某个变量。

  2. hooks 不再要求「状态必须在 this.state 中声明,生命周期写在 componentDidMount/componentDidUpdate 中」。useState/useEffect 的写法更加自由,我们可以按照页面功能/特性来组织 hooks 的书写位置。按照特性的代码组织方式 能够显著提升新需求的开发效率。

  3. 和 2 类似,自定义 hooks 提供了更加灵活的逻辑复用机制。自定义 hooks 带来了更多的可能性,例如 swr 是一个异步数据加载的自定义 hooks,提供了 Fast page navigation、Revalidation on focus、Interval polling、Request deduplication 等特性,这个在 class 的写法下几乎是无法实现的。又如像 react-use 这样的类库,若能适当在业务中用起来,可以显著提升开发效率。

当组件/页面变得更为复杂时,hooks 会比 class 写法带来更大的心智负担的:stale closure、过多的 re-render、useEffect 依赖膨胀、缺少 getDerivedStateFromProps …… 这个时候就不能像原来那样自由发挥 hooks 了,还是老老实实用回 class 写法了。

会不会重新触发 render?

这个确实是我刚接触时的一个痛点,毕竟写惯了 class-style 的组件,作为一个高工不写两行代码在 shouldComponentUpdate 简直对不起自己。然后问题就来了,在 hook 中就会总想会不会重复渲染啊这类的问题。

后来发现,其实完全没有必要。因为 hook 的 useEffect 本质上就是为了模糊生命周期以及渲染周期的概念的,只要 render 函数中不存在副作用、消耗较小(适当的 memo),多渲染几次其实没什么问题的。这里唯一值得注意的是,render 中千万要小心副作用,如果不用 useEffect 基本上都会产生问题。

useEffect 和 useLayoutEffect 的区别

useEffect 里面的操作需要处理 DOM,并且会改变页面的样式,就需要用这个,否则可能会出现出现闪屏问题, useLayoutEffect 里面的 callback 函数会在 DOM 更新完成后立即执行,但是会在浏览器进行任何绘制之前运行完成,阻塞了浏览器的绘制.

谈谈 useLayoutEffect 和 useEffect 具体执行时机

useLayoutEffect 和平常写的 ClassComponent 的'componentDidMount'和'componentDidUpdate'同时执行。

useEffect 会在本次更新完成后,也就是第 1 点的方法执行完成后,在开启一次任务调度,在下次任务调度中执行 useEffect。

useRuduce 定义

const [state, dispatch] = useReducer(reducer, initState);
useReducer接收两个参数:
第一个参数:reducer函数,没错就是我们上一篇文章介绍的。
第二个参数:初始化的state。返回值为最新的state和dispatch函数(用来触发reducer函数,计算对应的state)。按照官方的说法:对于复杂的state操作逻辑,嵌套的state的对象,推荐使用useReducer。听起来比较抽象,我们先看一个简单的例子:

// 官方 useReducer Demo
    // 第一个参数:应用的初始化
    const initialState = {count: 0};

    // 第二个参数:state的reducer处理函数
    function reducer(state, action) {
        switch (action.type) {
            case 'increment':
              return {count: state.count + 1};
            case 'decrement':
               return {count: state.count - 1};
            default:
                throw new Error();
        }
    }

    function Counter() {
        // 返回值:最新的state和dispatch函数
        const [state, dispatch] = useReducer(reducer, initialState);
        return (
            <>
                // useReducer会根据dispatch的action,返回最终的state,并触发rerender
                Count: {state.count}
                // dispatch 用来接收一个 action参数「reducer中的action」,用来触发reducer函数,更新最新的状态
                <button onClick={() => dispatch({type: 'increment'})}>+</button>
                <button onClick={() => dispatch({type: 'decrement'})}>-</button>
            </>
        );
    }

最后我们总结一下这篇文章的一些主要内容:使用 reducer 的场景

  • 如果你的 state 是一个数组或者对象
  • 如果你的 state 变化很复杂,经常一个操作需要修改很多 state
  • 如果你希望构建自动化测试用例来保证程序的稳定性
  • 如果你需要在深层子组件里面去修改一些状态(关于这点我们下篇文章会详细介绍)
  • 如果你用应用程序比较大,希望 UI 和业务能够分开维护

useRef

动机

1.函数组件访问 DOM 元素;

2.函数组件访问之前渲染变量。

函数组件每次渲染都会被执行,函数内部的局部变量一般会重新创建,利用 useRef 可以访问上次渲染的变量,类似类组件的实例变量效果。

使用

  1. 每次渲染useRef返回值都不变;
  2. ref.current发生变化并不会造成re-render;
  3. ref.current发生变化应该作为Side Effect(因为它会影响下次渲染),所以不应该在render阶段更新current属性。
  4. 不可以在 render 里更新 ref.current 值,在异步渲染里render阶段open in new window可能会多次执行。
  5. 可以在 render 里更新 ref.current 值,上只要保证每次render不会造成意外效果,都可以在render阶段更新ref.current。但最好别这样,容易造成问题,useRef懒初始化open in new window毕竟是个特殊的例外open in new window
  6. ref.current 不可以作为其他 hooks(useMemo, useCallback, useEffect)依赖项
  7. ref 作为其他 hooks(useMemo, useCallback, useEffect)依赖项

为什么 ReactHooks 中不能有条件判断

  1. 初次渲染的时候,按照 useState,useEffect 的顺序,把 state,deps 等按顺序塞到 memoizedState 数组中。
  2. 更新的时候,按照顺序,从 memoizedState 中把上次记录的值拿出来。

Q:为什么只能在函数最外层调用 Hook?为什么不要在循环、条件判断或者子函数中调用。

A:memoizedState 数组是按 hook 定义的顺序来放置数据的,如果 hook 顺序变化,memoizedState 并不会感知到。

Q:自定义的 Hook 是如何影响使用它的函数组件的?

A:共享同一个 memoizedState,共享同一个顺序。

Q:“Capture Value” 特性是如何产生的?

A:每一次 ReRender 的时候,都是重新去执行函数组件了,对于之前已经执行过的函数组件,并不会做任何操作

在两个组件中使用相同的 Hook 会共享 state 吗?

不会。自定义 Hook 是一种重用状态逻辑的机制(例如设置为订阅并存储当前值),所以每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。

Hooks 与链表???

hooks 原理,真实使用的是链表,而不是数组?

useContext

可以深层组件传值,父组件传给子孙组件。接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。

useReducer

useState 的替代方案,可以用于复杂状态处理。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。

增强我们的 Reducer,实现类似 Redux 的功能;

接受 (state, action) ⇒ newState 的一个 reduce,并返回当前的 state 以及与其配套的 dispatch 方法;

useImperativeHandle

useImperativeHandle(ref, createHandle, [deps]);

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用:

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

有没有自己实现过相关 hooks?说明一下实现思路?

自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。

封装 HTTP 请求

Hooks 为什么要用数组

Hooks 的本质就是一个数组。 那么为什么 hooks 要用数组? 我们可以换个角度来解释,如果不用数组会怎么样?

function Form() {
  // 1. Use the name state variable
  const [name, setName] = useState("Mary");

  // 2. Use an effect for persisting the form
  useEffect(function persistForm() {
    localStorage.setItem("formData", name);
  });

  // 3. Use the surname state variable
  const [surname, setSurname] = useState("Poppins");

  // 4. Use an effect for updating the title
  useEffect(function updateTitle() {
    document.title = name + " " + surname;
  });

  // ...
}

基于数组的方式,Form 的 hooks 就是 [hook1, hook2, hook3, hook4],

我们可以得出这样的关系, hook1 就是[name, setName] 这一对,hook2 就是 persistForm 这个。

如果不用数组实现,比如对象,Form 的 hooks 就是

{
  'key1': hook1,
  'key2': hook2,
  'key3': hook3,
  'key4': hook4,
}

那么问题是 key1,key2,key3,key4 怎么取呢? 最主要的问题是没法保证 Hooks 获取的顺序了。

关于 React hooks 的本质研究,更多请查看React hooks: not magic, just arraysopen in new window

React 将如何确保组件内部 hooks 保存的状态之间的对应关系这个工作交给了 开发人员去保证,即你必须保证 HOOKS 的顺序严格一致,具体可以看 React 官网关于 Hooks Rule 部分。

自己实现一个请求的 Hook

const useRequest = (request: () => Promise<any>) => {
  const [loading, setLoading] = useState(false);
  const [result, setResult] = useState<any>(null);
  const [error, setError] = useState<any>(null);

  const aa = useCallback(async () => {
    setLoading(true);
    try {
      const result = await request();
      setLoading(false);
      setResult(result);
    } catch (err) {
      setLoading(false);
      setError(err);
    }
  }, [request]);

  useEffect(() => {
    aa();
  }, [aa]);

  return { loading, result, error };
};
const App = () => {
  const { loading, result, error } = useRequest(() =>
    fetch("https://www.api.com").then((response) => response.json())
  );

  console.log(loading, result, error);

  return <div>{JSON.stringify(result)}</div>;
};

如何确保钩子遵循正确的使用规则?

React 团队发布了一个名为eslint-plugin-react-hooks的 ESLint 插件,它实施了这两个规则。您可以使用以下命令将此插件添加到项目中,

npm install eslint-plugin-react-hooks@next

并在您的 ESLint 配置文件中应用以下配置:

// Your ESLint configuration
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error"
  }
}

注意: 此插件在 Create React App 已经默认配置。

note

以前在使用 react 的时候,我们总纠结于组件是否可以不要有内部状态,而选择 Class Component 还是 Function Component

说一说 react 的 hooks 的作用和对它的理解

我需要用 hooks 重写所有类组件吗?

不需要。但你可以在某些组件(或新组件)中尝试使用 hooks,而无需重写任何已存在的代码。因为在 ReactJS 中目前没有移除 classes 的计划。

如何使用 React Hooks 获取数据?

名为 useEffect 的 effect hook 可用于使用 axios 从 API 中获取数据,并使用 useState 钩子提供的更新函数设置组件本地状态中的数据。让我们举一个例子,它从 API 中获取 react 文章列表。

import React, { useState, useEffect } from "react";
import axios from "axios";

function App() {
  const [data, setData] = useState({ hits: [] });

  useEffect(async () => {
    const result = await axios(
      "http://hn.algolia.com/api/v1/search?query=react"
    );

    setData(result.data);
  }, []);

  return (
    <ul>
      {data.hits.map((item) => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}

export default App;

记住,我们为 effect hook 提供了一个空数组作为第二个参数,以避免在组件更新时再次激活它,它只会在组件挂载时被执行。比如,示例中仅在组件挂载时获取数据。

Hooks 是否涵盖了类的所有用例?

Hooks 并没有涵盖类的所有用例,但是有计划很快添加它们。目前,还没有与不常见的 getSnapshotBeforeUpdate 和 componentDidCatch 生命周期等效的钩子。

Hooks 需要遵循什么规则?

为了使用 hooks,你需要遵守两个规则:

仅在顶层的 React 函数调用 hooks。也就是说,你不能在循环、条件或内嵌函数中调用 hooks。这将确保每次组件渲染时都以相同的顺序调用 hooks,并且它会在多个 useState 和 useEffect 调用之间保留 hooks 的状态。 仅在 React 函数中调用 hooks。例如,你不能在常规的 JavaScript 函数中调用 hooks。

Hooks 应用 createContext

createContext 传参是一个默认值, defaultValue 未匹配 Provider 时生效 React.createContext(defaultValue)

ProductContext.Provider 标签注入一个 value 供子组件使用. useContext 方法和 context 提供的 Consumer 组件都可以用来获取 context

Hooks 库

  1. ahooks
  2. swr
Last Updated:
Contributors: yiliang114